iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
AI Engineering

Harness Engineering × Pi Agent 實戰:打造可觀測、可評估的 AI Coding Agent系列 第 28 篇

Day28:把 Coding Agent 放進 Docker:執行成本很低,憑證與網路邊界呢?

  • 分享至 

  • xImage
  •  

今天要回答的問題

Day26 量到字串比對的閘門會擋錯、會被繞過;Day27 說明真正的邊界是沙箱,而 Pi 選擇不內建,改成讓你換掉執行環境。

今天實際做一次,量三件事:

  1. 把 agent 關進容器,成功率會不會掉?
  2. 要多花多少錢、多少時間?
  3. 這層隔離到底隔離了什麼?

Day27 說過,Gondolin 那條路在 Windows 上走不通(Linux micro-VM + QEMU),所以這裡用 Docker:不是把工具路由進容器,而是把整個 pi 程序關進去。

第一個發現:image 裡要裝的不是 agent 需要的東西

我一開始以為容器裡只要有 Node 和 Pi 就好。錯了。

受測專案的驗收指令是 python scripts/check.py,它會跑 ruff 和 pytest。如果容器裡沒有 Python、沒有 ruff、沒有 pytest,agent 根本無法完成任務——它不是被隔離,是被廢掉。

所以 image 要裝的是「任務需要的東西」,而且版本要跟主機對齊,否則兩組比較的就不只是隔離:

FROM python:3.13-slim-bookworm
ARG NODE_VERSION=24.18.0
ARG PI_VERSION=0.84.3

RUN apt-get install -y ca-certificates curl git xz-utils ripgrep fd-find
RUN curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
    && tar -xJf "node-v${NODE_VERSION}-linux-x64.tar.xz" -C /usr/local --strip-components=1
RUN npm install -g "@earendil-works/pi-coding-agent@${PI_VERSION}"
# 跟主機同版本,check.py 的行為才一致
RUN pip install --no-cache-dir ruff==0.15.5 pytest==8.3.4

裝 ripgrep 和 fd 也是刻意的:Day13 提過 Pi 第一次啟動會自己去下載這兩個工具。不先裝好,每一次容器執行都會重下載一次。

成品 828 MB。這是隔離的第一筆帳,而且它跟 agent 沒什麼關係——是你的專案的形狀決定的。

第二筆帳:憑證

容器裡的 Pi 要呼叫模型就需要憑證。Day27 引過官方文件給的兩條路:把 API key 傳進去,或是掛載主機的設定目錄。

我選了掛載,因為這個系列用的是 ChatGPT 帳號的 OAuth 登入,沒有可以單獨傳的 key:

docker run --rm \
  -v <run 的工作目錄>:/work \
  -v <session 輸出目錄>:/session \
  -v <repo>:/repo:ro \
  -v D:\pi-agent:/pi-agent \        # ← 主機的 Pi 憑證
  -w /work -e PI_CODING_AGENT_DIR=/pi-agent \
  pi-bench:0.84.3 \
  node /usr/local/lib/.../cli.js -p --mode json --model ... -- "<任務敘述>"

這一行 -v D:\pi-agent:/pi-agent 就是 Day27 那句話的實體:

隔離不是免費的:你關住 agent 的同時,也要把它需要的東西一件一件送進去,而每送一件,邊界就少一分。

後面會回來算這筆帳。

量測台怎麼支援兩種跑法

這是我最滿意的部分:量測台幾乎不用改。

條件多一個欄位:

@dataclass(frozen=True)
class Condition:
    ...
    # 把整個 harness 程序放進這個容器跑,而不是主機(Day 28)
    docker_image: str | None = None

adapter 裡只多一個分支,而且指令本身一模一樣,只有路徑換掉:

inner = self.build_command(
    prompt, condition,
    PurePosixPath("/session"),          # 主機上是 run_dir/session
    cli_js="/usr/local/lib/node_modules/.../cli.js",
    repo_root=PurePosixPath("/repo"),   # 主機上是 REPO_ROOT
)
return ["docker", "run", "--rm", *mounts, condition.docker_image, *inner]

實驗設定檔就只差一個鍵:

"conditions": [
  {"name": "host"},
  {"name": "docker", "docker_image": "pi-bench:0.84.3"}
]

這是 Day7 那個「harness 無關的 runner」設計第一次真的回本。 當初把「怎麼叫起一個 agent」抽成 adapter,本來是為了以後能換 harness,結果先換掉的是「在哪裡跑」。

結果:量不出差別

任務用 T2 新增 endpoint,每組 5 次,gpt-5.6-luna,thinking medium。

每次執行的成本

條件 成功 成本中位數 tokens 中位數 工具呼叫中位數 耗時中位數 跑了 check.py
主機 5/5 $0.0073 68,417 28 52s 5/5
容器 5/5 $0.0081 57,264 25 55s 5/5

用 Day23 的尺來看:

比較 差異 95% CI(bootstrap) 有把握?
成本 +11.6% −28.2% ~ +41.9% 否
耗時 +6.4% −28.8% ~ +26.3% 否

兩個區間都含 0。 加上容器本身的啟動時間我單獨量過——docker run 一個什麼都不做的指令是 0.58 秒,相對於 52 秒的執行根本看不見。

所以第一、二個問題的答案是:

成功率沒掉,成本與時間的差異量不出來。執行期的隔離代價,在這個尺度上接近零。

一個值得單獨講的意外

注意上表一個矛盾的地方:容器組的 tokens 比較少(57K vs 68K),成本卻比較高。

拆開來看就懂了:

input(原價) cacheRead(十分之一價) 合計
主機 23,594 42,496 68,417
容器 26,544 33,280 57,264

容器組的快取命中比較少,所以總 token 少了 16%,帳單反而高一點。Day14 量過 cacheRead 的單價只有 input 的十分之一,這裡就是那件事的後果。

比 token 數不等於比成本。 這也是 Day23 選成本當主指標的原因——它已經把快取折算進去了。

(為什麼容器組快取命中較少?我沒有量到因果。合理的猜測是兩組的工具呼叫序列不同,導致前綴不同;但 n=5,這也可能純粹是隨機。我不打算硬掰。)

第三個問題:這層隔離到底隔離了什麼

這是今天最重要的一段,因為說不清楚邊界的隔離,比沒有隔離更危險。

狀態
主機的其他檔案(家目錄、其他專案) ✅ 看不到
主機安裝的軟體、PATH、環境變數 ✅ 看不到
主機的行程 ✅ 看不到
/work(任務目錄) ❌ 掛進來的,寫入直接落到主機硬碟
/repo(本專案) ❌ 掛進來的,唯讀
/pi-agent(我的 Pi 憑證) ❌ 掛進來的,可讀可寫
對外網路 ❌ 預設 bridge,完全暢通
容器內的身分 ⚠️ root

講白話:

一個惡意的 agent 在這個容器裡,仍然可以讀走我的 Pi 憑證,然後用暢通的網路把它送出去。

這不是 Docker 的漏洞,是我為了免登入自己開的門。Day27 那句「每送一件,邊界就少一分」——這就是那一件。

要把門關小,有幾個現成做法,但每一個都有代價:

做法 代價
改用短期 API key,不掛整個憑證目錄 這個系列用 OAuth 登入,沒有可單獨傳的 key
/pi-agent 掛成唯讀 token 過期時無法自動更新
--network 限制只能連模型端點 要維護一份端點清單
--read-only rootfs + tmpfs 要先確認 Pi 的暫存寫在哪
不要用 root 掛進來的 Windows 目錄權限要另外處理

我這次都沒做,因為今天的目的是量「同一件事在兩個地方跑的差別」,多加一項就多一個變數。但如果是真的要讓 agent 自動跑在 CI 上,這張表就是待辦清單。

帶得走的結論

一、執行期的隔離幾乎免費,建置期不是。
828 MB 的 image、跟主機對齊的四個工具版本、一份憑證的處置決定——這些才是成本,而且它們是一次性的、可以被寫進 Dockerfile 的。

二、容器裡要裝的是任務的相依,不是 agent 的相依。
這件事很容易漏。agent 只需要 Node;但沒有 ruff 和 pytest,它連「我做完了嗎」都回答不了。

三、邊界要能一條一條列出來。
上面那張表才是這篇的產出。「我有用 Docker」不是安全宣稱;「/pi-agent 是可讀可寫掛進去的、網路暢通、容器內是 root」才是。

誠實的邊界

  1. n=5、單一任務。 「量不出差別」不等於「沒有差別」,只是 n=5 看不到。長任務、大量檔案 I/O 的任務,bind mount 的效能差異可能就跑出來了。
  2. Windows + WSL2 的 Docker Desktop。 Linux 原生 Docker 的 I/O 路徑不一樣,數字不能直接搬。
  3. 沒有比 Gondolin。 它是 Linux micro-VM,我這台機器上跑不了(Day27 說明過)。
  4. 我沒有測「惡意 agent」。 上面那張邊界表是我讀設定推出來的,不是攻擊測試出來的。

Dockerfile 與完整資料:docker/Dockerfile、experiments/results/e28_sandbox/。

明天

Day29 回頭講一件從 Day18 就一直在用、但我從來沒認真介紹過的東西:Pi 的 session 檔。它不是對話存檔,而是一份可以重新 fold 成狀態的事件記錄——而這個性質,讓我在最後一週抓到了一個自己的量測 bug,還不用重跑任何一次實驗。


上一篇
Day27:字串規則擋不住繞路:Agent 沙箱真正該隔離的是什麼?
下一篇
Day29:不用重跑 160 次實驗:如何從 Session 事件重算成本與狀態
系列文
Harness Engineering × Pi Agent 實戰:打造可觀測、可評估的 AI Coding Agent 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言